use std::cmp::Ordering;

use aspect_ident::DeviceIdent;
use chrono::NaiveDateTime;
use sea_orm::prelude::*;
use sea_orm::{
    ActiveValue, EntityOrSelect, IntoActiveModel, QueryOrder, QuerySelect, TransactionTrait,
};
use tracing::*;

use aspect_codec::prelude::*;
use aspect_db_sql::{device as sqldevice, user_device};
use aspect_ident::device::{self, EquivocationReport};

use crate::errors::*;

/// Response from a database representing a query for a device identity.
#[derive(Clone, Debug)]
pub enum DirEntry {
    Pkg(device::DirPackage),
    Equiv(device::EquivocationReport),
}

impl DirEntry {
    pub fn ident(&self) -> &device::DeviceIdent {
        match self {
            Self::Pkg(pkg) => pkg.last_update().ident(),
            Self::Equiv(equiv) => equiv.ident(),
        }
    }

    pub fn seqno(&self) -> Option<u32> {
        match self {
            Self::Pkg(pkg) => Some(pkg.last_update().seqno()),
            Self::Equiv(eqv) => eqv.seqno(),
        }
    }
}

aspect_codec::inst_enum_codec! {
    DirEntry u8 {
        0 => Pkg(field device::DirPackage),
        1 => Equiv(field device::EquivocationReport),
    }
}

pub struct DeviceDatastore {
    dbc: DatabaseConnection,
}

impl DeviceDatastore {
    pub fn new(dbc: DatabaseConnection) -> Result<Self, Error> {
        Ok(DeviceDatastore { dbc })
    }

    /// Tries to save a dir entry, making sure not to break invariants.  Assumed
    /// that the signatures on all of the DIRs are correct.  This also includes
    /// setting a corresponding user ID for the DIR in the process.  Assumes
    /// that the DIR entry provided is valid and passes signature checks.
    ///
    /// If we have a "better" entry in the database (like a device record with
    /// a higher sequence number) or construct one (like an equivocation report
    /// from trying to insert a conflicting DIR, we return that instead).
    pub async fn try_insert_dir_entry(&self, dir: &DirEntry) -> Result<Option<DirEntry>, Error> {
        // TODO try to flatten this function out a bit

        let ident = dir.ident();
        let ident_raw = ident.as_ref().to_vec();
        let dir_enc = aspect_codec::encode_to_vec(dir)?;
        let seqno = dir.seqno().map(|v| v as i64);
        let new_is_equiv = matches!(dir, DirEntry::Equiv(_));

        // Start a transaction since we may do a write that's dependent on value
        // we read.
        let tx = self.dbc.begin().await?;
        let res = sqldevice::Entity
            .select()
            .filter(sqldevice::Column::DeviceId.eq(ident_raw.clone()))
            .order_by_desc(sqldevice::Column::Seqno)
            .one(&tx)
            .await?;

        // Construct this so it's ready when we want to use it.  The compiler
        // should be able to defer construction if we don't actually use it.
        let mut device_am = sqldevice::ActiveModel {
            device_id: ActiveValue::Set(ident_raw),
            seqno: ActiveValue::Set(seqno),
            is_equivocation: ActiveValue::Set(new_is_equiv),
            // don't set record yet so we can refer to dir_enc later
            ..Default::default()
        };

        let out = match res {
            // If there's no existing entry for this device, we can just insert.
            None => {
                device_am.is_most_recent_known = ActiveValue::Set(true);
                device_am.record = ActiveValue::Set(dir_enc);
                device_am.insert(&tx).await?;
                Ok(None)
            }

            Some(cur) => {
                match (cur.is_equivocation, new_is_equiv) {
                    (true, _) => {
                        // If there's an equivocation, then we should just return
                        // that instead of inserting something since it means we no
                        // longer trust the device.
                        Ok(Some(aspect_codec::decode::<DirEntry>(&cur.record)?))
                    }

                    (false, true) => {
                        // If the new one is an equivocation we can just insert that one too.
                        device_am.is_most_recent_known = ActiveValue::Set(true);
                        device_am.record = ActiveValue::Set(dir_enc);
                        device_am.insert(&tx).await?;

                        // And clear the current flag on the old one.
                        let mut current_am = cur.into_active_model();
                        current_am.is_most_recent_known = ActiveValue::Set(false);
                        current_am.update(&tx).await?;

                        Ok(None)
                    }

                    (false, false) => {
                        // Now we know they're both update packages we should
                        // have sequence numbers for both of them to compare.
                        let cur_seqno = cur.seqno.expect("db: cur DIR not eqv but has no seqno");
                        let new_seqno = seqno.expect("db: new DIR not eqv but has no seqno");

                        match i64::cmp(&cur_seqno, &new_seqno) {
                            Ordering::Less => {
                                // Happy case, we can just insert the new one
                                // and unset the flag for the old one.
                                device_am.is_most_recent_known = ActiveValue::Set(true);
                                device_am.record = ActiveValue::Set(dir_enc);
                                device_am.insert(&tx).await?;

                                let mut cur_am = cur.into_active_model();
                                cur_am.is_most_recent_known = ActiveValue::Set(false);
                                cur_am.update(&tx).await?;

                                Ok(None)
                            }

                            Ordering::Equal => {
                                // This is the weird path that we need to check
                                // for an equivocation.

                                // If the payloads are the same then this we're all
                                // fine and can exit early.
                                if cur.record == dir_enc {
                                    return Ok(None);
                                }

                                // The seqnos are the same, that might be an issue.
                                let Ok(DirEntry::Pkg(cur_pkg)) =
                                    aspect_codec::decode::<DirEntry>(&cur.record)
                                else {
                                    // This implies that the seqno field mismatched
                                    // the kind of device stored.
                                    panic!("db: malformed database row for device");
                                };

                                let DirEntry::Pkg(new_pkg) = &dir else {
                                    panic!("db: dir not package in package code path???");
                                };

                                // Try to construct an equiv report from the
                                // packages we have now.
                                match EquivocationReport::from_pkgs(new_pkg, &cur_pkg) {
                                    Ok(eqv) => {
                                        let eqv_enc = aspect_codec::encode_to_vec(&eqv)?;
                                        device_am.record = ActiveValue::Set(eqv_enc);
                                        device_am.is_most_recent_known = ActiveValue::Set(true);
                                        device_am.insert(&tx).await?;

                                        // The previous one is no longer the
                                        // most recent since we're constructing
                                        // an equivocation.
                                        let mut cur_am = cur.into_active_model();
                                        cur_am.is_most_recent_known = ActiveValue::Set(false);
                                        cur_am.update(&tx).await?;

                                        Ok(Some(DirEntry::Equiv(eqv)))
                                    }

                                    Err(e) => {
                                        // Then this is actually fine.  Do
                                        // nothing.  We probably would have
                                        // ruled this out already.
                                        warn!(%ident, err = %e, "repeated DIR insert caught late, this is weird, no equivocation");
                                        Ok(None)
                                    }
                                }
                            }

                            Ordering::Greater => {
                                // Dumb path, we just return the cur one.
                                Ok(Some(aspect_codec::decode::<DirEntry>(&cur.record)?))
                            }
                        }
                    }
                }
            }
        };

        tx.commit().await?;
        out
    }

    /// Queries a specific device entry.
    pub async fn query_dir_entry(
        &self,
        id: &device::DeviceIdent,
    ) -> Result<Option<DirEntry>, Error> {
        let idbuf = id.to_vec();

        let m = sqldevice::Entity::find()
            .filter(sqldevice::Column::DeviceId.eq(idbuf))
            .order_by_desc(sqldevice::Column::Seqno)
            .limit(1)
            .one(&self.dbc)
            .await?
            .ok_or(Error::UnknownIdent(*id))?;

        let ent = aspect_codec::decode::<DirEntry>(&m.record)?;
        Ok(Some(ent))
    }

    /// Adds a device to a user if not already registered.
    pub async fn add_device_to_user(
        &self,
        id: &device::DeviceIdent,
        uid: i32,
    ) -> Result<(), Error> {
        let idbuf = id.to_vec();
        let tx = self.dbc.begin().await?;

        let found = user_device::Entity::find()
            .filter(user_device::Column::DeviceId.eq(idbuf.clone()))
            .limit(1)
            .one(&tx)
            .await?;

        // If it's already registered then we should report who registered them.
        if let Some(found_m) = found {
            return Err(Error::DeviceAlreadyClaimed(*id, found_m.user_id));
        }

        let user_device_am = user_device::ActiveModel {
            user_id: ActiveValue::Set(uid),
            device_id: ActiveValue::Set(id.to_vec()),
            added: ActiveValue::Set(get_now_ndt()),
            ..Default::default()
        };

        user_device_am.insert(&tx).await?;

        tx.commit().await?;
        Ok(())
    }

    /// Reports all of the valid device entries, returning the most recent
    /// instances of each.
    pub async fn dump_valid_devices(&self, local: bool) -> Result<Vec<DirEntry>, Error> {
        // TODO add a range parameter

        if local {
            warn!("queried for local devices but we don't support this directly anymore");
        }

        let clause = sqldevice::Column::IsMostRecentKnown
            .eq(true)
            .and(sqldevice::Column::IsEquivocation.eq(false));

        let mm = sqldevice::Entity::find()
            .filter(clause)
            .all(&self.dbc)
            .await?;

        let mut ents = Vec::new();
        for m in mm {
            // TODO Skip invalid devices?
            ents.push(aspect_codec::decode::<DirEntry>(&m.record)?);
        }

        Ok(ents)
    }

    /// Reports all of the valid device entries, returning the most recent
    /// instances of each in some range.
    pub async fn dump_valid_devices_range(
        &self,
        start: DeviceIdent,
        end: DeviceIdent,
    ) -> Result<Vec<DirEntry>, Error> {
        let clause = sqldevice::Column::DeviceId
            .gte(start.to_vec())
            .add(sqldevice::Column::DeviceId.lt(end.to_vec()))
            .and(
                sqldevice::Column::IsMostRecentKnown
                    .eq(true)
                    .and(sqldevice::Column::IsEquivocation.eq(false)),
            );

        let mm = sqldevice::Entity::find()
            .filter(clause)
            .all(&self.dbc)
            .await?;

        let mut ents = Vec::new();
        for m in mm {
            // TODO Skip invalid devices?
            ents.push(aspect_codec::decode::<DirEntry>(&m.record)?);
        }

        Ok(ents)
    }
}

fn get_now_ndt() -> NaiveDateTime {
    chrono::Utc::now().naive_utc()
}
